import os

import torch
from tqdm import tqdm

from utils.utils import get_lr

from nets.KLD.KLD_loss import compute_distillation_output_loss, distillation_output_MSEloss


def fit_one_epoch(t_model_train, model_train, model, yolo_loss, loss_history, eval_callback, optimizer, epoch, epoch_step, epoch_step_val, gen, gen_val, Epoch, cuda, fp16, scaler, save_period, save_dir, local_rank=0,
                  teacher_model_use=0):
    loss        = 0
    val_loss    = 0

    if local_rank == 0:
        print('Start Train')
        pbar = tqdm(total=epoch_step,desc=f'Epoch {epoch + 1}/{Epoch}',postfix=dict,mininterval=0.3)

    #   加入教师model
    if teacher_model_use != 0:
        t_model_train.train()

    model_train.train()
    for iteration, batch in enumerate(gen):
        if iteration >= epoch_step:
            break

        images, targets = batch[0], batch[1]
        with torch.no_grad():
            if cuda:
                images  = images.cuda(local_rank)
                targets = [ann.cuda(local_rank) for ann in targets]
        #----------------------#
        #   清零梯度
        #----------------------#
        optimizer.zero_grad()
        if not fp16:
            #----------------------#
            #   前向传播
            #----------------------#
            outputs         = model_train(images)


            # print(len(outputs))  #4

            loss_value_all  = 0
            #----------------------#
            #   计算损失
            #----------------------#
            for l in range(len(outputs)):
                t_l = len(outputs) - l - 1
                loss_item = yolo_loss(l, outputs[l], targets)

                if teacher_model_use != 0:
                    # ----------------------#
                    #   蒸馏损失
                    # ----------------------#
                    # print("\noutput" + str(l))
                    # print(outputs[l].size())
                    # bs = outputs[l].size(0)
                    # for b in range(bs):
                    #     print(outputs[l][b].size())


                    # print(len(outputs[l][0]))
                    # print(t_outputs[t_l][0].size())
                    # [batch_size, 3*(5+num_classes), 52,52]
                    # d_outputs_loss = compute_distillation_output_loss(outputs[l], t_outputs[l], model, d_weight=10)
                    # d_outputs_loss = distillation_output_MSEloss(outputs[l], t_outputs[l])
                    # print(d_outputs_loss)
                    # print(len(outputs))

                    loss_value_all += loss_item
                else:
                    loss_value_all  += loss_item

            # ---------------------------#
            #   加入知识蒸馏
            # ---------------------------#
            if teacher_model_use != 0:
                with torch.no_grad():
                    t_outputs = t_model_train(images)
                    d_outputs_loss = distillation_output_MSEloss(outputs, t_outputs)
                    # print(d_outputs_loss)
                    # print('--------------------')
                loss_value = loss_value_all + d_outputs_loss

            else:
                loss_value = loss_value_all
                #----------------------#
            #   反向传播
            #----------------------#
            loss_value.backward()
            optimizer.step()
        else:
            from torch.cuda.amp import autocast
            with autocast():
                #----------------------#
                #   前向传播
                #----------------------#
                outputs         = model_train(images)

                # ---------------------------#
                #   加入知识蒸馏
                # ---------------------------#
                if teacher_model_use != 0:
                    with torch.no_grad():
                        t_outputs = t_model_train(images)

                loss_value_all  = 0
                #----------------------#
                #   计算损失
                #----------------------#
                for l in range(len(outputs)):
                    loss_item = yolo_loss(l, outputs[l], targets)

                    if teacher_model_use != 0:
                        # ----------------------#
                        #   蒸馏损失
                        # ----------------------#
                        d_outputs_loss = compute_distillation_output_loss(outputs[l], t_outputs[l], model, d_weight=10)
                        loss_value_all += loss_item + d_outputs_loss
                    else:
                        loss_value_all += loss_item

                loss_value = loss_value_all

            #----------------------#
            #   反向传播
            #----------------------#
            scaler.scale(loss_value).backward()
            scaler.step(optimizer)
            scaler.update()

        loss += loss_value.item()
        
        if local_rank == 0:
            pbar.set_postfix(**{'loss'  : loss / (iteration + 1), 
                                'lr'    : get_lr(optimizer)})
            pbar.update(1)

    if local_rank == 0:
        pbar.close()
        print('Finish Train')
        print('Start Validation')
        pbar = tqdm(total=epoch_step_val, desc=f'Epoch {epoch + 1}/{Epoch}',postfix=dict,mininterval=0.3)

    model_train.eval()
    for iteration, batch in enumerate(gen_val):
        if iteration >= epoch_step_val:
            break
        images, targets = batch[0], batch[1]
        with torch.no_grad():
            if cuda:
                images  = images.cuda(local_rank)
                targets = [ann.cuda(local_rank) for ann in targets]
            #----------------------#
            #   清零梯度
            #----------------------#
            optimizer.zero_grad()
            #----------------------#
            #   前向传播
            #----------------------#
            outputs         = model_train(images)

            loss_value_all  = 0
            #----------------------#
            #   计算损失
            #----------------------#
            for l in range(len(outputs)):
                loss_item = yolo_loss(l, outputs[l], targets)
                loss_value_all  += loss_item
            loss_value  = loss_value_all

        val_loss += loss_value.item()
        if local_rank == 0:
            pbar.set_postfix(**{'val_loss': val_loss / (iteration + 1)})
            pbar.update(1)
 
    if local_rank == 0:
        pbar.close()
        print('Finish Validation')
        loss_history.append_loss(epoch + 1, loss / epoch_step, val_loss / epoch_step_val)
        eval_callback.on_epoch_end(epoch + 1, model_train)
        print('Epoch:'+ str(epoch + 1) + '/' + str(Epoch))
        print('Total Loss: %.3f || Val Loss: %.3f ' % (loss / epoch_step, val_loss / epoch_step_val))
        
        #-----------------------------------------------#
        #   保存权值
        #-----------------------------------------------#
        if (epoch + 1) % save_period == 0 or epoch + 1 == Epoch:
            torch.save(model.state_dict(), os.path.join(save_dir, "ep%03d-loss%.3f-val_loss%.3f.pth" % (epoch + 1, loss / epoch_step, val_loss / epoch_step_val)))

        if len(loss_history.val_loss) <= 1 or (val_loss / epoch_step_val) <= min(loss_history.val_loss):
            print('Save best model to best_epoch_weights.pth')
            torch.save(model.state_dict(), os.path.join(save_dir, "best_epoch_weights.pth"))
            
        torch.save(model.state_dict(), os.path.join(save_dir, "last_epoch_weights.pth"))